NOTE

1.7 Redis Lua

Redis Lua usage, debugging, data-type conversion, supported libraries, logging, execution time, EVAL/EVALSHA, IDEs, and example scripts.

Redis / CacheCreated Updated 3 min readhistorical

This is a historical learning note and may contain outdated or incomplete understanding.

1. Lua Introduction

Lua

2. Using Redis Lua

2.1. Execute a Lua Script

redis-cli  --eval test4.lua  shop_point shop_id_list , 1 1600482998753

Here shop_point and shop_id_list are KEYS, while 1 and 1600482998753 are ARGV. KEYS and ARGV are separated by ,.

2.2. Debug a Lua Script

  • By default, a new session is started, meaning the server is not blocked; the script-debugging session is rolled back after completion.
redis-cli  --ldb --eval test4.lua  shop_point shop_id_list , 1 1600482998753
  • If synchronous mode is needed, add --ldb-sync-mode. In this mode, the Redis server cannot be accessed.

2.2.1. Common Commands

  • quit: terminate the session.
  • restart: restart the debugging session from the beginning and reload the new version of the script from the file.
  • help: help list.
  • In the session:
    • redis.debug() is equivalent to watch, while print outputs variables to the console.
    • b line-number sets a breakpoint, b -line-number removes a breakpoint, and b 0 removes all breakpoints.
      • Dynamic breakpoint: if counter > 10 then redis.breakpoint() end.

2.3. Execute Redis Commands in Lua

  • redis.call vs. redis.pcall The difference is that if a Redis command fails, redis.call throws the error outward for the caller to handle, while redis.pcall only returns the error to the Lua script for handling.

2.4. Redis and Lua Data-Type Conversion

Rule: Redis type -> Lua type -> Redis type. If the initial value and final value are the same, it can be converted.

2.4.1. Redis -> Lua

Redis integer reply -> Lua number
Redis bulk reply -> Lua string
Redis multi bulk reply -> Lua table (may have other Redis data types nested)
Redis status reply -> Lua table with a single ok field containing the status
Redis error reply -> Lua table with a single err field containing the error
Redis Nil bulk reply and Nil multi bulk reply -> Lua false boolean type

2.4.2. Lua -> Redis

Lua number -> Redis integer reply (the number is converted into an integer)
Lua string -> Redis bulk reply
Lua table (array) -> Redis multi bulk reply (truncated to the first nil inside the Lua array if any)
Lua table with a single ok field -> Redis status reply
Lua table with a single err field -> Redis error reply
Lua boolean false -> Redis Nil bulk reply.

2.5. Libraries Supported by Redis Lua

  • base lib.
  • table lib.
  • string lib.
  • math lib.
  • struct lib.
  • cjson lib.
  • cmsgpack lib.
  • bitop lib.
  • redis.sha1hex function.
  • redis.breakpoint and redis.debug function in the context of the Redis Lua debugger.

2.6. Redis Lua Logging

  • Configure the log path and log level in redis.conf.
# 2. Specify the server verbosity level.
# 3. This can be one of:
# 4. debug (a lot of information, useful for development/testing)
# 5. verbose (many rarely useful info, but not a mess like the debug level)
# 6. notice (moderately verbose, what you want in production probably)
# 7. warning (only very important / critical messages are logged)
loglevel notice

# 8. Specify the log file name. Also 'stdout' can be used to force
# 9. Redis to log on the standard output. 
logfile "redis.log"
  • Use redis.log(loglevel,message) to print logs.
    • Log levels:
    redis.LOG_DEBUG
    redis.LOG_VERBOSE
    redis.LOG_NOTICE
    redis.LOG_WARNING
    • message: string.format can be used for formatted output.

2.7. Redis Lua Execution Time

  • Lua scripts generally finish at the millisecond level. The maximum execution time defaults to 5 seconds and can be configured with lua-time-limit in redis.conf.
  • After 5 seconds, Redis does not terminate the Lua script; it only records it in the log.

2.8. EVAL vs. EVALSHA

Because Redis does not guarantee persistence or replication of Lua-script cache, Redis may still clear the Lua-script cache in some scenarios, such as instance migration or configuration changes. This requires the client to be able to handle this error. Lua Script Usage Guidelines

3. Redis Lua IDE

3.1. ZeroBrane

ZeroBrane Studio - Lua IDE/editor/debugger for Windows, Mac OSX, and Linux

4. Redis Lua Examples

4.1. Recording Points

  • Use a string to record the point list.
local shopPointKey = KEYS[1]
local shopIdListKey = KEYS[2]
local currentShopId = ARGV[1]
local ts = ARGV[2]

-- checkSetCurrentPoint
local function checkCurrentPoint()
    local shopJson = redis.call("HGET", shopPointKey, currentShopId)
    redis.log(redis.LOG_NOTICE, "checkCurrentPoint: get point for shopPointKey=", shopPointKey, ", currentShopId=", currentShopId, "result=", shopJson)
    if shopJson then
        local shop = cjson.decode(shopJson)
        if shop.start_time ~= 0 then
            return 10001
        end
    end
    return 0
end

-- checkSetPrevPoint
local function checkSetPrevPoint()
    local newShopList = {}
    local shopIdList = {}
    local preShopIndex = 0

    local shopIdListjson = redis.call("GET", shopIdListKey)
    redis.log(redis.LOG_NOTICE, "checkSetPrevPoint: get shop id list for shopIdListKey=", shopIdListKey, "result=", shopIdListjson)

    if shopIdListjson then
        shopIdList = cjson.decode(shopIdListjson)
        preShopIndex = table.getn(shopIdList)
        for i, v in pairs(shopIdList) do
            if v ~= currentShopId then
                table.insert(newShopList, v)
            end
        end
    end

    table.insert(newShopList, currentShopId)

    if preShopIndex ~= 0 then
        local preShopId = shopIdList[preShopIndex]
        local preShopJson = redis.call("HGET", shopPointKey, preShopId)
        redis.log(redis.LOG_NOTICE, "checkSetPrevPoint: get pre shop for shopPointKey=", shopPointKey, "preShopId=", preShopId, "result=", preShopJson)
        if preShopJson then
            local preShop = cjson.decode(preShopJson)
            if (preShop.end_time == nil) or (preShop.end_time == 0) then
                preShop.end_time = ts
                local preShopJsonNew = cjson.encode(preShop)
                redis.call("HSET", shopPointKey, preShopId, preShopJsonNew)
                redis.log(redis.LOG_NOTICE, "checkSetPrevPoint: set pre shop end time for shopPointKey=", shopPointKey, "preShopId=", preShopId, "result=", preShopJsonNew)
            end
        end
    end

    return newShopList, 0
end

-- setCurrentPoint
local function setCurrentPoint()
    local currentShopPoint = {
        start_time = ts
    }
    local currentShopPointJson = cjson.encode(currentShopPoint)
    redis.call("HSET", shopPointKey, currentShopId, currentShopPointJson)
    redis.log(redis.LOG_NOTICE, "setCurrentPoint: set current shop start time for shopPointKey=", shopPointKey, "currentShopId=", currentShopId, "result=", currentShopPointJson)
    return 0
end

-- setShopList
local function setShopList(newShopList)
    local newShopListJson = cjson.encode(newShopList)
    redis.call("SET", shopIdListKey, newShopListJson)
    redis.log(redis.LOG_NOTICE, "setShopList: update shop list for shopIdListKey=", shopIdListKey, "newShopListJson=", newShopListJson)
end

local function main()

    redis.log(redis.LOG_NOTICE, "set point: request. keys=", shopPointKey, shopIdListKey, ", argv=", currentShopId, ts)

    --local ret = checkCurrentPoint()
    --if ret ~= 0 then
    --    redis.log(redis.LOG_WARNING, "set point: checkCurrentPoint error. ret=", ret)
    --    return ret
    --end

    local newShopList, ret = checkSetPrevPoint()
    if ret ~= 0 then
        redis.log(redis.LOG_WARNING, "set point: checkCurrcheckSetPrevPointentPoint error. ret=", ret)
        return ret
    end

    ret = setCurrentPoint()
    if ret ~= 0 then
        redis.log(redis.LOG_WARNING, "set point: setCurrentPoint error. ret=", ret)
        return ret
    end

    setShopList(newShopList)

    return 0
end

return main()
  • Use a list to record the point list.
local shopPointKey = KEYS[1]
local shopIdListKey = KEYS[2]
local currentShopId = ARGV[1]
local ts = ARGV[2]

-- checkSetCurrentPoint
local function checkCurrentPoint()
    local shopJson = redis.call("HGET", shopPointKey, currentShopId)
    redis.log(redis.LOG_NOTICE,
            string.format("checkCurrentPoint: get current point. keys=%s,%s, result=%s",
                    shopPointKey, currentShopId, shopJson))
    if shopJson then
        local shop = cjson.decode(shopJson)
        if shop.start_time ~= 0 then
            return 10001
        end
    end
    return 0
end

-- checkSetPrevPoint
local function checkSetPrevPoint()

    local preShopId = redis.call("LINDEX", shopIdListKey, -1)
    redis.log(redis.LOG_NOTICE, string.format("checkSetPrevPoint: get pre shop id. key=%s, result=%s",
            shopIdListKey, preShopId))
    if preShopId then
        local preShopJson = redis.call("HGET", shopPointKey, preShopId)
        redis.log(redis.LOG_NOTICE, string.format("checkSetPrevPoint: get pre shop. key=%s,%s, result=%s",
                shopPointKey, preShopId, preShopJson))
        if preShopJson then
            local preShop = cjson.decode(preShopJson)
            if (preShop.end_time == nil) or (preShop.end_time == 0) then
                preShop.end_time = ts
                local preShopJsonNew = cjson.encode(preShop)
                redis.call("HSET", shopPointKey, preShopId, preShopJsonNew)
                redis.log(redis.LOG_NOTICE,
                        string.format("checkSetPrevPoint: set pre shop end time. key=%s,%s, result=%s",
                        shopPointKey, preShopId, preShopJsonNew))
            end
        end
    end

    return 0
end

-- setCurrentPoint
local function setCurrentPoint()
    local currentShopPoint = {
        start_time = ts
    }
    local currentShopPointJson = cjson.encode(currentShopPoint)
    redis.call("HSET", shopPointKey, currentShopId, currentShopPointJson)
    redis.log(redis.LOG_NOTICE,
            string.format("setCurrentPoint: set current shop start time. key=%s,%s, result=%s",
            shopPointKey, currentShopId, currentShopPointJson))
    return 0
end

-- updateShopList
local function updateShopList()
    redis.call("LREM", shopIdListKey, 1, currentShopId)
    redis.call("RPUSH", shopIdListKey, currentShopId)
    redis.log(redis.LOG_NOTICE, string.format("updateShopList: update shop list. key=%s,%s",
            shopIdListKey, currentShopId))
end

local function main()

    redis.log(redis.LOG_NOTICE, string.format("set point: request params: keys=%s, %s, argv=%s, %s",
            shopPointKey, shopIdListKey, currentShopId, ts))

    --local ret = checkCurrentPoint()
    --if ret ~= 0 then
    --    redis.log(redis.LOG_WARNING,
    --            string.format("set point: checkCurrentPoint error: ret=%d", ret))
    --    return ret
    --end

    local ret = checkSetPrevPoint()
    if ret ~= 0 then
        redis.log(redis.LOG_WARNING,
                string.format("set point: checkCurrcheckSetPrevPointentPoint error: ret=%d", ret))
        return ret
    end

    ret = setCurrentPoint()
    if ret ~= 0 then
        redis.log(redis.LOG_WARNING,
                string.format("set point: setCurrentPoint error: ret=%d", ret))
        return ret
    end

    updateShopList()

    return 0
end

return main()

4.2. Add a Key

local wantToSeeKeysKey = KEYS[1]
local wantToSeeKey = KEYS[2]

-- existsKey
local function existsKey()
    local existsFlag = false
    local res = redis.call("LRANGE", wantToSeeKeysKey, 0, -1)
    redis.log(redis.LOG_NOTICE, "existsKey: wantToSeeKeysKey=", wantToSeeKeysKey, "res=", res)

    if res then
        for i, v in pairs(res) do
            if v == wantToSeeKey then
                existsFlag = true
                break
            end
        end
    end

    return existsFlag
end

-- main
local function main()
    redis.log(redis.LOG_NOTICE, "addWantToSeeKeys: req params: keys=", wantToSeeKeysKey, wantToSeeKey)

    if existsKey() then
        redis.log(redis.LOG_WARNING, "addWantToSeeKeys: key exitst")
        return 10001
    end

    redis.call("RPUSH", wantToSeeKeysKey, wantToSeeKey)
    redis.log(redis.LOG_NOTICE, "addWantToSeeKeys: add", wantToSeeKey, "to", wantToSeeKeysKey)

    return 0
end

return main()

4.3. Initialize the Number of Seckill Participants

local activityKey = KEYS[1]
local goodsId = ARGV[1]
local participants = tonumber(ARGV[2])
local expireTs = tonumber(ARGV[3])

local function isActivityKeyExists()
    return redis.call("EXISTS", activityKey)
end

local function setActivityKeyExpireTs()
    return redis.call("EXPIRE", activityKey, expireTs)
end

local function incrGoodsParticipant()
    return redis.call("HINCRBY", activityKey, goodsId, participants)
end

local function getGoodsParticipant()
    return redis.call("HGET", activityKey, goodsId)
end

local function main()
    if isActivityKeyExists() == 0 then
        local goodsParticipants = incrGoodsParticipant()
        setActivityKeyExpireTs()
        return goodsParticipants
    end

    local goodsParticipants = getGoodsParticipant()
    if goodsParticipants and goodsParticipants ~= 0 then
        return goodsParticipants
    else
        return incrGoodsParticipant()
    end
end

return main()

5. QA

5.1. CROSSSLOT Keys in request don’t hash to the same slot

Cluster

6. References

Discussion

Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub